Skip to content

Triggers: support managing columns for data iteration#7541

Open
labkey-nicka wants to merge 20 commits intodevelopfrom
fb_trigger_columns
Open

Triggers: support managing columns for data iteration#7541
labkey-nicka wants to merge 20 commits intodevelopfrom
fb_trigger_columns

Conversation

@labkey-nicka
Copy link
Copy Markdown
Contributor

@labkey-nicka labkey-nicka commented Apr 2, 2026

Rationale

When query triggers are called within data iterators they are allowed to manipulate the data of the row that is being procured. However, if a trigger modifies the columns (i.e., the keys of the row map) it can result in data being mismanaged due to the columns in the row not aligning with what the data iterator specified.

This introduces the notion of "managed" columns to triggers. Triggers can, at the outset, declare which columns they are going to manage. Any managed columns are then picked up by the data iterator during initialization to ensure they are persisted correctly. To enforce this management more strictly, and to avoid mismanagement of null values, triggers are required to fulfill all managed column values by either clearing the value or setting the value. After the trigger has fired the resulting row will be validated and checked. The result is consistent enforcement and acknowledgement of triggers that manipulate the shape of the data.

Related Pull Requests

Changes

  • Declare TableInfo.getTriggerManagedColumns() to allow for data iterator to gather all managed columns
  • Add difference checks for non-managed columns added or removed by triggers
  • Support managed columns in server-side JS trigger scripts
  • Add utilities to ensure managed column values are set (setInsertManagedColumns() and setUpdateManagedColumns())
  • Add experimental feature flag which when enabled will disable the managed trigger columns feature

Tasks

  • Initial review @labkey-matthewb
  • Hook up server-side JS triggers
  • Only throw errors in development for non-data iterator triggers
  • Add experimental feature flag to disable
  • Optimize row checks
  • Needs automation
  • Code review @mbellew
  • Manual testing @XingY
  • Verify fix

Manual Testing notes (in progress)

  • DataIterator is not able to recognize column labels, but managedColumns does? @labkey-nicka
    1. Repro: create a string key list, add another property "With Space" (with a space in the color name)
    2. Use the following trigger script:
    function beforeUpdate(row, errors) {
        row['WithSpace'] = 'true';
    }
    
    function managedColumns() {
        return {
            update: ['WithSpace']
        }
    }
    1. Use query api to update a single row, update is successful and correctly sets "With Space" to 'true'.
    2. Use bulk update, update is successful, but "With Space" is not set to 'true'.
  • Study dataset: the presence of a trigger script result in existing QCState erased using "Import with Update" (Not merge, which is a different known issue).
    • Unrelated to this branch: reproduces on develop
  • Study dataset: the presence of the following trigger script setup result in inability to update a dataset row from detail edit (non data iterator). The update page refreshes on submit, without error or success feedback, the data is not actually updated
    • Unrelated to this branch: reproduces on develop
function beforeUpdate(row, errors) {
    row['QC State'] = 'clean';
}

function managedColumns() {
    return {
        update: ['QC State']
    }
}
  • Lists (but maybe other data type as well): if the trigger script provides an invalid value for the managed column, for example, a string value for an int field. The import fails with a "false" error msg.
    • Unrelated to this branch: reproduces on develop
  • Not sure if related to this branch (data integrity): trigger script only works for column name, not column label. However, if the trigger script uses label (such as 'Status' for 'SampleState'), then that results in existing SampleState being set to blank.
    • Unrelated to this branch: reproduces on develop
    • Seems like this is the same as this issue (current critical issue)
  • Trigger script doesn't work for parent input columns for sources and samples
    • Unrelated to this branch: reproduces on develop

@labkey-nicka labkey-nicka self-assigned this Apr 2, 2026
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considerations for additional features (do we want to do now or never)?

  • Support different managed columns for insert/update
  • Support in JavaScript triggers

@labkey-nicka labkey-nicka modified the milestones: 26.04, 26.05 Apr 3, 2026
@labkey-nicka labkey-nicka marked this pull request as ready for review April 3, 2026 17:15
@labkey-nicka
Copy link
Copy Markdown
Contributor Author

Support different managed columns for insert/update
Support in JavaScript triggers

Implemented both of these.

if (errors.hasErrors())
break;

if (trackedRow != null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add manageColumns check to this check? if (trackedRow != null && manageColumns)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want to inform/check for changes regardless of the state of the manageColumns flag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. LOG.warn()

Copy link
Copy Markdown
Contributor Author

@labkey-nicka labkey-nicka Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've revised this check to no longer apply to the non-data iterator configuration. We now will only check for managed columns when in a data iterator (and before is true and experimental flag is true). No more logging warnings as encountering this in a data iterator is always considered an error.

@labkey-matthewb labkey-matthewb self-requested a review April 7, 2026 18:26
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the error checking, and merging in the values for the managed columns early is an interesting idea that probably greatly reduces the chance of losing data.

@labkey-matthewb labkey-matthewb self-requested a review April 7, 2026 18:31
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small suggestion. I like the strict error checking.

return;

if (oldRow == null)
throw new IllegalArgumentException("An existing record must be supplied for all UPDATE triggers");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are no managed columns, can oldRow be null? It seems the caller all have oldRow as Nullable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "An existing record" is ambiguous here. oldRow and existingRecord are different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be consistent in how this is referred to in error messaging, not necessarily with the code name. Intentional.

var triggerColumns = _target.getTriggerManagedColumns(_c, context.getInsertOption());
if (triggerColumns != null && !triggerColumns.isEmpty())
{
var columns = triggerColumns.stream().map(_target::getColumn).filter(Objects::nonNull).toList();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on manual testing, we should probably skip resolving column here.getColumn(columnName, false /*don't try to resolve*/)

Either that, or DataIterator needs to be re-worked to support column name resolving for values coming from trigger, which seems bigger than simply don't allow it.

@bbimber
Copy link
Copy Markdown
Collaborator

bbimber commented Apr 10, 2026

hey @labkey-nicka: this sounds like a good improvement, but can you confirm how a couple of scenarios will work under this:

  • If the table does not declare any columns as managed, does anything change from present day behavior?

  • If a table declares a set of managed columns, and if the trigger script adds a new property to a row in the beforeInsert/Update that is not part of the managed columns, will this unexpected property still be passed to the insert/update code? Should any behavior on non-managed properties change relative to current day?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants